/*
 * Copyright (c) 2013 Johann Hanne
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <stdint.h>
#include <portio.h>

#include "i2c.h"

#define SDA_BIT 0x0010
#define SCL_BIT 0x0020

void nop(void);
#pragma aux nop = "nop";
#define I2C_DELAY()  \
    do { \
        nop(); nop(); nop(); nop(); nop(); \
    } while (0)

void i2c_init(void) {
   /* Set I2C IO pins to output mode */
   iow_clearbits(0x0f66, SDA_BIT | SCL_BIT);
   /* Set I2C IO pins high (=I2C idle) */
   iow_setbits(0x0f64, SDA_BIT | SCL_BIT);
}

inline static uint16_t get_sda(void) {
    iow_setbits(0x0f66, SDA_BIT);
    return inpw(0x0f64) & SDA_BIT;
}

inline static uint16_t get_scl(void) {
    iow_setbits(0x0f66, SCL_BIT);
    return inpw(0x0f64) & SCL_BIT;
}

inline static void set_sda_high() {
    iow_clearbits(0x0f66, SDA_BIT);
    iow_setbits(0x0f64, SDA_BIT);
}

inline static void set_scl_high() {
    iow_clearbits(0x0f66, SCL_BIT);
    iow_setbits(0x0f64, SCL_BIT);
}

inline static void set_sda_low() {
    iow_clearbits(0x0f66, SDA_BIT);
    iow_clearbits(0x0f64, SDA_BIT);
}

inline static void set_scl_low() {
    iow_clearbits(0x0f66, SCL_BIT);
    iow_clearbits(0x0f64, SCL_BIT);
}

static void write_start_condition(void) {
    set_sda_low();
    I2C_DELAY();
    set_scl_low();
    I2C_DELAY();
}

static void write_restart_condition(void) {
    set_sda_high();
    set_scl_high();
    I2C_DELAY();
    set_sda_low();
    I2C_DELAY();
    set_scl_low();
    I2C_DELAY();
}

static i2c_status_t write_stop_condition(void) {
    set_sda_low();
    I2C_DELAY();
    set_scl_high();
    I2C_DELAY();
    set_sda_high();
    I2C_DELAY();

    if (get_scl() == 0) {
        return I2C_SCL_STUCK_LOW;
    }
    if (get_sda() == 0) {
        return I2C_SDA_STUCK_LOW;
    }

    return I2C_OK;
}

static i2c_status_t write_byte(uint8_t byte, int writing_address) {
    i2c_status_t i2c_status = I2C_OK;
    uint8_t bit;

    for (bit = 0x80; bit != 0; bit >>= 1) {
        if ((byte & bit) != 0) {
            set_sda_high();
        } else {
            set_sda_low();
        }

        I2C_DELAY();
        set_scl_high();
        I2C_DELAY();
        set_scl_low();
    }

    set_sda_high();
    set_scl_high();
    I2C_DELAY();

    /* Acknowledge from slave? */
    if (get_sda() != 0) {
        i2c_status = (writing_address ? I2C_NACK_ON_ADDRESS : I2C_NACK_ON_DATA);
    }

    set_scl_low();
    I2C_DELAY();

    return i2c_status;
}

static i2c_status_t read_byte(uint8_t *byte, int write_ack) {
    i2c_status_t i2c_status = I2C_OK;
    uint8_t bit;

    *byte = 0;

    for (bit = 0x80; bit != 0; bit >>= 1) {
        set_scl_high();
        I2C_DELAY();

        if (get_sda() != 0) {
            (*byte) |= bit;
        }

        set_scl_low();

        if (bit == 0x01 && write_ack) {
            set_sda_low();
        }

        I2C_DELAY();
    }

    set_scl_high();
    I2C_DELAY();
    set_scl_low();
    set_sda_high();
    I2C_DELAY();

    return i2c_status;
}

i2c_status_t i2c_write_restart_read(uint8_t addr_w, uint8_t *data_w, size_t len_w,
        uint8_t addr_r, uint8_t *data_r, size_t len_r) {
    i2c_status_t status = I2C_OK;
    int do_write = 0, do_read = 0;
    size_t i;

    /* For scan operation, both lengths can be set to zero; we will perform
       a zero-length write for this */
    if (len_w > 0 || len_r == 0) {
        do_write = 1;
    }

    if (len_r > 0) {
        do_read = 1;
    }

    write_start_condition();

    if (do_write) {
        /* Write address byte */
        status = write_byte(addr_w << 1, 1);
        if (status != I2C_OK) {
            goto stop;
        }
        /* Write data bytes */
        for (i = 0; i < len_w; i++) {
            status = write_byte(data_w[i], 0);
            if (status != I2C_OK) {
                goto stop;
            }
        }

        if (do_read) {
            write_restart_condition();
        }
    }

    if (do_read) {
        /* Write address byte (bit 0x01 indicates read operation) */
        status = write_byte((addr_r << 1) | 0x01, 1);
        if (status != I2C_OK) {
            goto stop;
        }
        /* Last byte is read outside the loop because we must not
           acknowledge it */
        len_r -= 1;
        for (i = 0; i < len_r; i++) {
            status = read_byte(data_r + i, 1);
            if (status != I2C_OK) {
                goto stop;
            }
        }
        status = read_byte(data_r + len_r, 0);
        if (status != I2C_OK) {
            goto stop;
        }
    }

stop:

    if (status == I2C_OK) {
        status = write_stop_condition();
    } else {
        write_stop_condition();
    }

    return status;
}
